#coding:utf-8
import weakref
import traceback
import time
import os
import sys
import time

import __builtin__ as gl


from modules import profilesManager
from modules import pluginsManager as plug
from modules import pipe
from modules import botsManager as BotManager
from modules import heartBeat
from modules import room
from modules import userItem as newUser

from modules.decorators import *


from modules import utils

from twisted.internet.task import LoopingCall

from twisted.words.protocols.jabber import xmlstream
from twisted.words.protocols.jabber import client
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.xmlstream import IQ, TimeoutError


from twisted.internet import protocol
from twisted.internet import defer
from twisted.internet.threads import deferToThread

from twisted.words.xish import domish
from twisted.python import log


log.startLogging(sys.stdout)

gl.ROLES = {
	"none"       : 0,
	"visitor"    : 0,
	"participant": 10,
	"moderator"  : 15
}

gl.AFFILIATIONS = {
	"outcast" : 0,
	"none"    : 0,
	"member"  : 1,
	"admin"   : 5,
	"owner"   : 15
}

 


class Subject(object):
	'''
		Контейнер агрегирующий объект стансы с объектом клиента
	'''
	__slots__ = ("__xml","__client")
	def __init__ (self,cl,xml):
		self.__xml = xml
		self.__client = client
	def __getattr__(self,name):
		'''
			перегруженная функция вызова методов клиента
		'''
		return self.__client.__getattr__(name)
	def element(self):
		'''
			возвращает объект стансы
		'''
		return self.__xml


def delete(*args):
        log.msg("delete me")




def callEvent(eventType,mode,*args,**kwargs):
	if mode is "sync":
		for func in plug.getEvents(eventType):
			func(*args,**kwargs)
	elif mode is "queue":
		for func in plug.getEvents(eventType):
			tPool.start(lambda:func(*args,**kwargs))




class KernelBot(object):
	def __init__(self,profile):
		self.profile = profile #настройки подключения и профиль в целом
		self.conferences = {} #словарь конференций в которых находится бот и их юзеры
		self.__xmlStreamSession = None #тут лежит ссылка на объект сессии
		self.__connection = None #ссылка самого подключения
		self.__state = "init" #состояние клиент
		self.selfJid = jid.JID(profile.FULL_JID)
		self.globalLock = threading.Lock()
		self.mainConfig = None
		self.profile = profile
		self.xping = heartBeat.HeartBeat(self) # запуск пинговалки аля KeepAlive
		self.clientFactory=client.XMPPClientFactory(self.selfJid,self.profile.PASSWORD) #инициализируем XMPP клиент
		import inspect
		for name,handle in inspect.getmembers(self,callable):
                        if hasattr(handle,"_bootstrap_func"):
                                self.clientFactory.addBootstrap(handle._bootstrap_func,handle)
		self.clientFactory.clientConnectionLost = self.connectionLost #соединение потеряно
		self.clientFactory.clientConnectionFailed = self.connectionFailed #ошибка соединения
		self.eventDisconnect = None
		self.eventConnect = None
		
        def dataRawIn(self,data):
                self.statistic["trafficIn"]+=len(data)
                
        def dataRawOut(self,data):
                self.statistic["trafficOut"]+=len(data) 
                
	@property
	def xmlstream(self):
		'''
			так как __xmlStreamSession есть слабая ссылка, то для извлечения объекта необходимо вызвать
			эту ссылку,для упрощения работы + гарантия целостности  read-only. readme weakref
		'''
		return self.__xmlStreamSession
	@property
	def state(self):
		'''
			возврат статуса клиента
		'''
		return self.__state
		
        @Bootstrap("//event/stream/authd")
	def _authSucess(self,xmlstream):
		'''
			результат успешной аутентификации
		'''
                import inspect
		print "AUTH OK!"
		self.statistic = {"err":0,"thr":0,"cmd":0,"trafficIn":0,"trafficOut":0,"startTime":time.time()}
		xmlstream.rawDataInFn = self.dataRawIn
		xmlstream.rawDataOutFn = self.dataRawOut
		self.__state = "connect"
		self.send(self.createPresenceNode(priority = self.profile.PRIORITY)) #установка статуса и приоритета ресурса поделючения
		self.__xmlStreamSession = weakref.proxy(xmlstream) #создаём слабую ссылку сессии
                for name,handle in inspect.getmembers(self,callable):
                        if hasattr(handle,"_observer_func"):
                                self.__xmlStreamSession.addObserver(handle._observer_func,handle)
                self.initBot()
                
        @Lock("globalLock")
        @MaybeCallInThread
        def initBot(self):
                self.mainConfig = utils.loadConfig("configs/main_config.ini",
                                {"conferences":[],
                                        "cmdacces":{},
                                        "accesses":{},
                                        "modulesconfig":{}})
                callEvent(plug.EVENT_STARTUP,"sync")
                for conference in iter(self.mainConfig.as_list("conferences")):
                        self.initConference(conference).join()
                        callEvent(plug.EVENT_INITCONFERENCE,"sync",conference)
                log.msg("Join in the {0} conferences".format(len(self.mainConfig["conferences"])))
                self.xping.start()
                
        @Bootstrap("//event/xmpp/initfailed")
	def _authFail(self,xmlstream):
		print "AUTH FAIL"
		
        @Bootstrap("//event/stream/connected")
	def _onConnected(self,xmlstream):
		self.__state = "establish_link"
		
        @Bootstrap("//event/stream/end")
	@isState(lambda st: st!="disconnect")
	def _endStream(self,xmlstream):
		'''
			<stream:stream/>
		'''
		self.disconnect()
		
        @Bootstrap("//event/stream/error")
	def _errorStream(self,xmlstream):
		'''
			Обработка ошибки сессии
		'''
		log.msg("Catch stream error!")
		try:
			el = xmlstream.value.getElement()
		except:
                        log.msg("Unhandled stream error!")
			return
                log.msg("Stream error : {0}".format(el.firstChildElement().name))
		if el.firstChildElement().name == 'conflict':
			reactor.callLater(0.5,self.connect) # в случае если ошибка "Resource conflict" то через 0.5 сек поделючение

	def connectionLost(self,connector,reason):
		'''
			Обработка потери поделючения
		'''
		log.msg("Connection lost : {0!r}".format(reason))
		self.disconnect()
		if callable(self.eventDisconnect):
                        reactor.callFromThread(self.eventDisconnect,self)

	def connectionFailed(self,connector,reason):
		'''
			Обработка ошибки поделючения
		'''
		print reason
		if self.__state == "init": #HACK
			self.connectionLost(connector,reason)
			return
                self.__state = "disconnect"
		reactor.callLater(10,self.connect)
	
	@CallInMainThread
        @Lock("globalLock")
	@isState(lambda st: st!="disconnect")
	def disconnect(self,safeConnect = True,restart = False,reason = None):
		'''
			Отключение
			Закрытие сессии
			Закрытие поделючения
		'''
		log.msg("Disconnect")
		self.__state = "disconnect"
		if reason:
                        self.__xmlStreamSession.send(u"<presence type='unavailable'><status>{0}</status></presence>".format(reason))
		self.__xmlStreamSession.sendFooter()
		try:
			self.xping.stop() # вырубаем пинговалку
		except Exception:
			pass
		self.clientFactory.stopTrying() # закрытие сессии
		if self.__connection:
			self.__connection.disconnect() # закрытие подключение
			if not safeConnect:
				self.__connection = None
		if restart:
			self.__state = "reconnect"
			reactor.callLater(1,self.connect)
		return callEvent(plug.EVENT_SHUTDOWN,"deferToThread",self)
	
	@CallInMainThread
        @Lock("globalLock")
	@isState(lambda st: st in ("disconnect","init"))
	def connect(self):
		'''
			Функция подключения
		'''
		self.__state = "do_connect"
		if self.__connection:
			self.__connection.connect()
		else:
			if self.profile.SSL:
                                pass
				from twisted.internet import ssl
				self.__connection=reactor.connectSSL(self.profile.HOST,self.profile.PORT,self.clientFactory,ssl.ClientContextFactory())
			else:
				self.__connection=reactor.connectTCP("c4.ciliz.com",5222,self.clientFactory)
                log.msg("Do connect")

        def initConference(self,conference):
                _room = room.roomItem(self,conference)
                self.conferences[conference] = _room
                return _room
                
        def freeConference(self,conference,save = False):
                if save:
                        self.conferences[conference].saveConfig()
                del self.conferences[conference]
                
        def createPresenceNode(self,priority,status=None,show=None,):
                presence=domish.Element((None,"presence"))
                presence.addElement("priority",content=str(priority))
                if not status is None:
                        presence.addElement("status",content=status)
                if not show is None:
                        presence.addElement("show",content=show)
                return presence
                
        def joinConference(self,conference,nick,priority=0,status=None,show=None,password=None):
                prsJoin = self.createPresenceNode(priority,status,show)
                prsJoin["to"] = "{conference}/{nick}".format(**locals())
                mucTag = prsJoin.addElement("x","http://jabber.org/protocol/muc")
                historyTag = mucTag.addElement("history")
                historyTag["maxchars"]="0"
                historyTag["maxstansas"]="0"
                if password:
                        mucTag.addElement("password",content=password)
                self.send(prsJoin)
                
        def leaveConference(self,conference,status=None):
                presence = domish.Element((None,"presence"))
                presence["type"] = "unavailable"
                presence["to"] = conference
                if status:
                        presence.addElement("status",content=status)
                self.send(presence)
                self.freeConference(conference)
                                
        @Lock("globalLock")
        @MaybeCallInThread
        def reloadMainConfig(self):
                self.mainConfig.reload()
                
        @CallInMainThread
        @execute
	def send(self,subject,to=None):
		'''
			Отправка стансы
		'''
		if to and not isinstance(subject,basestring):
			subject["to"] = to
                if self.__xmlStreamSession:
                        self.__xmlStreamSession.send(subject)  
                        
                
                
	def getGlobalAccess(self,jid):
                if jid and jid in self.mainConfig["accesses"]:
                        return self.mainConfig["accesses"].as_int(jid)

	def IQ(self,typ="set",to=None,timeout=None):
		iq = IQ(self.xmlstream,typ)
		iq.timeout = timeout
		if to:
			iq["to"] = to
		return iq
		
	def sendMsg(self,msgType,to,body):
		msg=domish.Element(('jabber:client', 'message'))
		msg["to"] = to
		msg["type"]=msgType
		msg.addElement("body",content = body)
		self.send(msg)
		                
        def isJoined(self,conference):
                return self.conferences.has_key(conference) 
                               

        @Observer("/iq")
        def onIqParse(self,element):
                if not element.hasAttribute("from"):
                        return
                if element.hasAttribute("id"):
                        iqResponse = domish.Element((None,"iq"))
                        iqResponse["to"] = element["from"]
                        iqResponse["id"] = element["id"]
                        if element.getAttribute("type") == "get":
                                childUri  = element.firstChildElement() and element.firstChildElement().uri or None
                                if childUri == "urn:xmpp:ping":
                                        iqResponse["type"] = "result"
                                        self.send(iqResponse)
                                        return
                        elif element["id"] in self.xmlstream.iqDeferreds and element.getAttribute("type") in ("result","error"):
                                return
                        iqResponse["type"]  = "error"
                        iqResponse["from"] = self.profile.FULL_JID
                        err = iqResponse.addElement("error")
                        err["code"] = "501"
                        err["type"] = "cancel"
                        err.addElement("feature-not-implemented")
                        self.send(iqResponse)
                else:
                        element["type"]  = "error"
                        element["to"] = el["from"]
                        element["from"] = self.profile.FULL_JID
                        err = element.addElement("error")
                        err["code"] = "400"
                        err["type"] = "modify"
                        err.addElement("bad-request")
                        self.send(element)
                        
			#callEvent(plug.EVENT_PRESENCE | plug.H_CONFERENCE,"queue",self,element,conference,nick,userJid)###FIXME!
			
	@Observer("/presence[not(@type) or 'available']/x[@xmlns='http://jabber.org/protocol/muc#user']")
	def onPresenceJoin(self,element):
                _jid = jid.JID(element["from"])
                conference = _jid.userhost()
                nick = _jid.resource
                if not self.isJoined(conference):
                        return
                prsNode = element | utils.getTag("x","http://jabber.org/protocol/muc#user")
                prsItems = prsNode | utils.getTag("item")
                userJid =  prsItems.getAttribute("jid")
                affiliation = prsItems.getAttribute("affiliation")
                role = prsItems.getAttribute("role")
                conferenceItem = self.conferences[conference]
                item = newUser.userItem(self,conferenceItem)
                conferenceItem[nick] = item
                if userJid:
                        item.fullJid   = userJid
                item.initAccess(AFFILIATIONS[affiliation]+ROLES[role])
                item.nickName=nick
                item.role         = role
                item.affiliation = affiliation
                item.joined     = time.time()
                item.isHere     = True
               #callEvent(plug.EVENT_USERONLINE,"queue",Subject(self,element),conference,nick,jid,affiliation,role)
                
        @Observer("/presence[@type='unavailable']/x[@xmlns='http://jabber.org/protocol/muc#user']")       
        def onPresenceLeave(self,element):
                _jid = jid.JID(element["from"])
                conference = _jid.userhost()
                if not self.isJoined(conference):
                        return
                nick = _jid.resource
                statusCode,status,reason = None,None,None
                userItem = self.conferences[conference][nick]
                if userItem.isHere is True:
                        prsNode = element | utils.getTag("x","http://jabber.org/protocol/muc#user")
                        prsItems = prsNode | utils.getTag("item")
                        userJid =  prsItems.getAttribute("jid")
                        statusNode = prsNode | utils.getTag("status")
                        reason = unicode((prsItems | utils.getTag("reason") ) or statusNode or u"")
                        if statusNode:
                                statusCode = statusNode["code"]
                                if statusCode == "303":
                                        newNick = prsItems["nick"]
                                        reason = newNick
                                        newItem = userItem.copy()
                                        newItem.nickName = newNick
                                        self.conferences[conference][newNick] = newItem
                                elif statusCode == "301":
                                        if userItem.conference.getBotNick() == nick:
                                                log.msg("I was banned in the {1} {0}".format(conference,reason and "with reason \"{0}\"".format(reason.encode("utf-8")) or ""))
                                                self.freeConference(conference)
                                                reactor.callInThread(self.mainConfig.write)
                                                return
                                elif statusCode == "307":
                                        if userItem.conference.getBotNick() == nick:
                                                log.msg("I was kicked in the {1} {0} . Rejoining...".format(conference,reason and "with reason \"{0}\"".format(reason.encode("utf-8")) or ""))
                                                reactor.callLater(0.5,userItem.join)
                                                return
                        userItem.isHere = False
                        for attr in ("idle", "affiliation", "role", "jid"):
                                setattr(userItem,attr,None)
                        
                #callEvent(plug.EVENT_USEROFFLINE,"queue",Subject(self,element),conference,nick,userJid,reason,statusCode)
                
        @Observer("/presence[@type='error']/error[@code]")
        def onPresenceError(self,element):
                conference = jid.JID(element["from"]).userhost()
                if self.isJoined(conference):
                        errorNode = element | utils.getTag("error")
                        errorCode = errorNode["code"]
                        reasonError = unicode(errorNode | utils.getTag("text") or "")
                        roomItem = self.conferences[conference]
                        log.msg("from {0} received error {1} {2}".format(conference,errorCode,reasonError))
                        if errorCode == "409":
                                lastNickName = roomItem.getBotNick()
                                newNickName = lastNickName + "."
                                log.msg("{0} : change nickname from {1} to {2}".format(conference,lastNickName,newNickName))
                                roomItem.config["nick"] = newNickName
                                roomItem.saveConfig()
                                reactor.callLater(1,roomItem.join)
                        if errorCode == "403":
                                self.leaveConference(conference,"Got error code {0}".format(errorCode))
                                self.mainConfig["conferences"].remove(conference)
                                reactor.callInThread(self.mainConfig.write)
                                
                        
	@Observer("/message[@type='chat']/body")		
        def onRosterMessage(self,element):
                msgFullJid = jid.JID(element["from"])
                bareJid = msgFullJid.userhost()
                if self.isJoined(bareJid):
                        self.onGroupchatMessage(element)
                        return
                userItem = newUser.userItem(self)
                userItem.fullJid = msgFullJid.full()
                userItem.initAccess(10)
                if userItem.access == -100:       
                        return
                if element | utils.getTag("request"):
                        msgReport = domish.Element((None,"message"))
                        msgReport["to"] = element["from"]
                        msgReport["id"] = element.getAttribute("id")
                        msgReport.addElement("received","urn:xmpp:receipts")
                        self.send(msgReport)
                messageContent = unicode( element | utils.getTag("body")).strip()
                if not messageContent:
                        return
                command,params = messageContent | utils.splitMessageQuery
                if plug.isCommand(command):
                        self.__callCommand(command, plug.CMD_ROSTER, element["type"], userItem, params)
                        
        @Observer("/message[@type='groupchat']/body")
        def onGroupchatMessage(self,element):
                msgFullJid = jid.JID(element["from"])
                bareJid = msgFullJid.userhost()
                nickName = msgFullJid.resource
                if not self.conferences.has_key(bareJid):
                        return
                userItem = self.conferences[bareJid].get(nickName)
                if userItem is None or userItem.access == -100:
                        return
                messageContent = element | utils.getTag("body")| utils.__str__
                messageContent = messageContent.strip()
                command,params = messageContent | utils.splitMessageQuery
                if plug.isCommand(command):
                        self.__callCommand(command, plug.CMD_CHAT, element["type"], userItem, params)
                        
        @Observer("/message[@type='error']/error[@code='406' or '500']")
        def onErrorMessage(self,element):
                errorNode = element | utils.getTag("error")
                errCode = errorNode["code"]
                def buildReply(element):
                        messageReply = domish.Element((None,"message"))
                        messageReply["to"] = element["from"]
                        messageReply["from"] = element["to"]
                        messageReply["type"] = "groupchat"
                        messageReply.addElement("body",content = unicode(element | utils.getTag("body")))
                        return messageReply
                if errCode == "406":
                        @reactor.callInThread
                        def unstoppable():
                                fromErr = jid.JID(element["from"])
                                reactor.callLater(0.5,self.send,buildReply(element))
                elif errCode == "500":
                        reactor.callLater(0.7,self.send,buildReply(element))
                        
        def __callCommand(self,command, conType, msgType, userItem, params):
                cmdHandle = plug.Commands[command]
                if conType & cmdHandle.cmdType:
                        if cmdHandle.access <= userItem.access:
                                        if not params and cmdHandle.cmdType & plug.CMD_PARAM:
                                                userItem.sendMsg(msgType,u"Для этой команды необходимы параметры\nВоспользуйтесь помощью по команде!")
                                                return
                                        if params and cmdHandle.cmdType & plug.CMP_NONPARAM:
                                                userItem(msgType,u"Для этой команды не требуются параметры\nВоспользуйтесь помощью по команде!")
                                                return
                                        reactor.callInThread(self.__call, cmdHandle, msgType, userItem, params)
                        else:
                                userItem.sendMsg(msgType,u"Недостаточно прав")
        @execute
        def __call(self,handle,*args,**kwargs):
                handle(*args,**kwargs)
                               


def run():
	plug.LoadPlugins("plugins")
	profilesManager.profileReg("profiles")
	for pr in profilesManager.getProfileIter():
		k = KernelBot(pr)
		BotManager.registry(k)
		k.connect()
	reactor.run()

if __name__ == "__main__":
        log.msg("Bot startup!")
	run()
